"use strict"
import "../../lib/ramda.js"
export const BOARD_SIZE = 9
export const P_EMPTY = 0
export const P_OU = 1                   // 王(玉)
export const P_HI = 2                   // 飞
export const P_KAKU = 3                 // 角
export const P_KIN = 4                  // 金
export const P_GIN = 5                  // 银
export const P_KEI = 6                  // 桂
export const P_KYO = 7                  // 香
export const P_FU = 8                   // 步
export const P_RYU = 9                  // 竜(龙)
export const P_UMA = 10                 // 龙马(马)
export const P_NARIGIN = 11             // 成银(全)
export const P_NARIKEI = 12             // 成桂(圭)
export const P_NARIKYO = 13             // 成香(杏)
export const P_TOKIN = 14               // と

export const TEBAN_GOTE = 1             // 上方(后手)
export const TEBAN_SENTE = 2            // 下方(先手)

export const STATUS_NORMAL = 0
export const STATUS_SENTE_WIN = 1
export const STATUS_GOTE_WIN = 2
export const STATUS_DRAW = 3

const YX_WAYS = [
    [-1,0], [1,0], [0,-1],[0,1],[-1,-1],[-1,1],[1,1],[1,-1]
]

const MOVE_MATS = [
    [],
    [1, 1, 1, 1, 1, 1, 1, 1],            // OU
    [9, 9, 9, 9, 0, 0, 0, 0],            // HI
    [0, 0, 0, 0, 9, 9, 9, 9],            // KAKU
    [1, 1, 1, 1, 1, 1, 0, 0],            // KIN
    [1, 0, 0, 0, 1, 1, 1, 1],            // GIN
    [0, 0, 0, 0, 0, 0, 0, 0],            // KEI
    [9, 0, 0, 0, 0, 0, 0, 0],            // KYOU
    [1, 0, 0, 0, 0, 0, 0, 0],            // FU
    [9, 9, 9, 9, 1, 1, 1, 1],            // RYU
    [1, 1, 1, 1, 9, 9, 9, 9],            // UMA
    [1, 1, 1, 1, 1, 1, 0, 0],            // NARIGIN
    [1, 1, 1, 1, 1, 1, 0, 0],            // NARIKEI
    [1, 1, 1, 1, 1, 1, 0, 0],            // NARIKYO
    [1, 1, 1, 1, 1, 1, 0, 0],            // TOKIN
]

const INHAND_TO = [
    0,
    0,              // OU
    P_HI,           // HI
    P_KAKU,         // KAKU
    P_KIN,          // KIN
    P_GIN,          // GIN
    P_KEI,          // KEI
    P_KYO,          // KYOU
    P_FU,           // FU
    P_HI,           // RYU
    P_KAKU,         // UMA
    P_GIN,          // NARIGIN
    P_KEI,          // NARIKEI
    P_KYO,          // NARIKYO
    P_FU,           // TOKIN
]

const NARI_TO = [
    0,
    P_OU,           // OU
    P_RYU,          // HI -> RYU
    P_UMA,          // KAKU -> UMA
    P_KIN,          // KIN
    P_NARIGIN,      // GIN -> NARIGIN
    P_NARIKEI,      // KEI -> NARIKEI
    P_NARIKYO,      // KYO -> NARIKYO
    P_TOKIN,        // FU -> TOKIN
    P_RYU,          // RYU
    P_UMA,          // UMA
    P_NARIGIN,      // NARIGIN
    P_NARIKEI,      // NARIKEI
    P_NARIKYO,      // NARIKYO
    P_TOKIN,        // TOKIN
]

const PIECE_TO_CHAR = [
    '-',
    "Aa",           // OU
    "Bb",           // HI
    "Cc",           // KAKU
    "Dd",           // KIN
    "Ee",           // GIN
    "Ff",           // KEI
    "Gg",           // KYO
    "Hh",           // FU
    "Ii",           // RYU
    "Jj",           // UMA
    "Kk",           // NARIGIN
    "Ll",           // NARIKEI
    "Mm",           // NARIKYO
    "Nn",           // TOKIN
]

const CHAR_TO_PIECE = {
    '-':P_EMPTY,
    'A':0x11, 'a':0x21,
    'B':0x12, 'b':0x22,
    'C':0x13, 'c':0x23,
    'D':0x14, 'd':0x24,
    'E':0x15, 'e':0x25,
    'F':0x16, 'f':0x26,
    'G':0x17, 'g':0x27,
    'H':0x18, 'h':0x28,
    'I':0x19, 'i':0x29,
    'J':0x1A, 'j':0x2A,
    'K':0x1B, 'k':0x2B,
    'L':0x1C, 'l':0x2C,
    'M':0x1D, 'm':0x2D,
    'N':0x1E, 'n':0x2E,
}

const TEBAN_TO_CHAR = ['S', 'G']
const CHAR_TO_TEBAN = {'S':TEBAN_SENTE, 'G':TEBAN_GOTE}

export function Piece(_class, teban) {
    return (teban << 4) | _class
}

export function Piece_class(p) {
    return (p & 0x0F)
}

export function Piece_teban(p) {
    return (p >> 4)
}

export function Piece_nari(p) {
    return Piece(NARI_TO[Piece_class(p)], Piece_teban(p))
}

export function Piece_is_nari(p) {
    return (Piece_class(p) > P_FU)
}

export function Piece_from_str(str) {
    return CHAR_TO_PIECE[str]
}

export function Piece_to_str(p) {
    if (p == P_EMPTY) {return PIECE_TO_CHAR[0]}
    return PIECE_TO_CHAR[Piece_class(p)][Piece_teban(p)-1]
}

export function teban_swap(teban) {
    return 3 - teban
}

export function teban_from_str(str) {
    return CHAR_TO_TEBAN[str]
}

export function teban_to_str(teban) {
    return TEBAN_TO_CHAR[2-teban]
}

export function Loc(y,x) {
    return (y << 4) | x
}

export function Loc_y(loc) {
    return (loc >> 4)
}

export function Loc_x(loc) {
    return (loc & 0x0F)
}

export function Loc_from_str(str) {
    const y = Number(str[0]) - 1
    const x = Number(str[1]) - 1
    return Loc(y, x)
}

export function Loc_to_str(loc) {
    const y = Loc_y(loc) + 1
    const x = Loc_x(loc) + 1
    return `${y}${x}`
}

export function yx_in_board(y,x) {
    return (y >= 0 && y < BOARD_SIZE) && (x >= 0 && x < BOARD_SIZE)
}

export function loc_in_board(loc) {
    return yx_in_board(Loc_y(loc), Loc_x(loc))
}


export class Move {
    constructor(piece, loc_to, loc_from=-1, nari=false) {
        this.piece = piece
        this.loc_to = loc_to
        this.loc_from = loc_from
        this.nari = nari
    }

    piece() {
        return this.piece
    }

    is_move() {
        return loc_in_board(this.loc_from)
    }

    is_put() {
        return (this.loc_from < 0)
    }

    is_nari() {
        return this.nari
    }

    to_str() {
        const pstr = Piece_to_str(this.nari ? Piece_nari(this.piece) : this.piece)
        if (loc_in_board(this.loc_from)) {
            return `${pstr}/${Loc_to_str(this.loc_to)}/${Loc_to_str(this.loc_from)}`
        } else {
            return `${pstr}/${Loc_to_str(this.loc_to)}`
        }
    }
}

export function Move_from_str(str) {
    const rows = str.split('/')
    const p = Piece_from_str(rows[0])
    const loc_to = Loc_from_str(rows[1])
    const loc_from = (rows.length > 2) ? Loc_from_str(rows[2]) : -1
    return new Move(p, loc_to, loc_from)
}

export class Board {
    constructor(_from=undefined) {
        if (! R.is(Board, _from)) {
            this._arr = new Uint8Array(BOARD_SIZE*BOARD_SIZE)
            this._inhands = new Uint8Array(14)
        }
        this.reset(_from)

    }

    reset(_from=undefined) {
        if (R.is(Board, _from)) {
            this._arr = new Uint8Array(_from._arr)
            this._inhands = new Uint8Array(_from._inhands)
            this._king_locs = _from._king_locs
            this._fu_bits = _from._fu_bits
        } else {
            this._arr.fill(P_EMPTY)
            this._inhands.fill(0)
            this._king_locs = 0
            this._fu_bits = 0
            if (R.is(String, _from)) {
                this._load_from_str(_from)
            }
        }
    }

    _load_from_str(str) {
        const lines = str.split('/')
        if (lines.length >= BOARD_SIZE) {
            for (let y = 0; y < BOARD_SIZE; y += 1) {
                for (let x = 0; x < BOARD_SIZE; x += 1) {
                    const p = CHAR_TO_PIECE[lines[y][x]]
                    const p_teban = Piece_teban(p)
                    const p_class = Piece_class(p)
                    const loc = Loc(y,x)
                    if (p_class == P_OU) {
                        this._set_king_loc(p_teban, loc)
                    } else if (p_class == P_FU) {
                        this._set_fu_exist(p_teban, x, true)
                    }
                    this.set(loc, p)
                }
            }
            if (lines.length > BOARD_SIZE) {
                for (const pchar of lines[BOARD_SIZE]) {
                    const p = Piece_from_str(pchar)
                    this._inhands_in(Piece_class(p), Piece_teban(p))
                }
            }
        }
    }

    _board_to_str(lines) {
        for (let y = 0; y < BOARD_SIZE; y += 1) {
            const row = []
            for (let x = 0; x < BOARD_SIZE; x += 1) {
                row.push(Piece_to_str(this.get(Loc(y,x))))
            }
            lines.push(row.join(''))
        }
    }

    _inhands_to_str(lines) {
        function with_teban(row, teban) {
            for (const inhand of this.get_inhands(teban)) {
                const pchar = PIECE_TO_CHAR[inhand[0]][teban-1]
                for (let i = 0; i < inhand[1]; i++) {
                    row.push(pchar)
                }
            }
        }
        const row = []
        with_teban.apply(this, [row, TEBAN_GOTE])
        with_teban.apply(this, [row, TEBAN_SENTE])
        lines.push(row.join(''))
    }

    to_str() {
        let lines = []
        this._board_to_str(lines)
        this._inhands_to_str(lines)
        return lines.join('/')
    }

    get(loc) {
        const i = (Loc_y(loc)*BOARD_SIZE) + Loc_x(loc)
        if (i < this._arr.length) {
            return this._arr[i]
        }
        return P_EMPTY
    }

    set(loc,p) {
        const i = (Loc_y(loc)*BOARD_SIZE) + Loc_x(loc)
        if (i < this._arr.length) {
            this._arr[i] = p
        }
        return this
    }

    _get_king_loc(teban) {
        const bit_move = 8 * (teban-1)
        return (this._king_locs & (0x00FF << bit_move)) >> bit_move
    }

    _set_king_loc(teban, loc) {
        const bit_move = 8 * (teban-1)
        this._king_locs &= 0xFF00 >> bit_move
        this._king_locs |= loc << bit_move
        return this
    }

    _get_fu_exist(teban, x) {
        const bit_move = (9 * (teban-1)) + x
        return ((this._fu_bits & (1 << bit_move)) >> bit_move)
    }

    _set_fu_exist(teban, x, is_exist) {
        const bit_move = (9 * (teban-1)) + x
        if (is_exist) {
            this._fu_bits |= 1 << bit_move
        } else {
            this._fu_bits &= ~(1 << bit_move)
        }
        return this
    }

    _inhands_in(p_class, teban_eater) {
        const p_class_ed = INHAND_TO[p_class]
        const i = (teban_eater == TEBAN_SENTE) ? p_class_ed-2 : p_class_ed+5
        this._inhands[i] += 1
        return this
    }

    _inhands_out(p_class, teban) {
        const i = (teban == TEBAN_SENTE) ? p_class-2 : p_class+5
        this._inhands[i] = Math.max(0, this._inhands[i]-1)
        return this
    }

    get_inhands(teban) {
        const inhands = []
        if (teban == TEBAN_SENTE) {
            for (let i = 0; i < 7; i += 1) {
                const n = this._inhands[i]
                if (n > 0) {
                    inhands.push([i+2, n])
                }
            }
        } else {
            for (let i = 7; i < 14; i += 1) {
                const n = this._inhands[i]
                if (n > 0) {
                    inhands.push([i-5, n])
                }
            }
        }
        return inhands
    }

    move(m) {
        if (loc_in_board(m.loc_to)) {
            if (m.is_move()) {
                const p_old = this.get(m.loc_to)
                const p_old_class = Piece_class(p_old)
                if (p_old == P_EMPTY) {
                    if (Piece_class(m.piece) == P_OU) {
                        this._set_king_loc(Piece_teban(m.piece), m.loc_to)
                    }
                } else if (p_old_class == P_OU) {
                    this._set_king_loc(Piece_teban(p_old), 0xFF)
                } else {
                    this._inhands_in(p_old_class, Piece_teban(m.piece))
                    if (p_old_class == P_FU) {
                        this._set_fu_exist(Piece_teban(p_old), Loc_x(m.loc_to), false)
                    }
                }
                this.set(m.loc_from, P_EMPTY).set(m.loc_to, (m.is_nari() ? Piece_nari(m.piece) : m.piece))
                return true
            } else if (m.is_put()) {
                const to_p = this.get(m.loc_to)
                if (to_p == P_EMPTY) {
                    const p_class = Piece_class(m.piece)
                    if (p_class == P_FU) {
                        if (this._get_fu_exist(Piece_teban(m.piece), Loc_x(m.loc_to))) {return  false}
                    }
                    this._inhands_out(p_class, Piece_teban(m.piece)).set(m.loc_to, m.piece)
                    return true
                }
            }
        }
        return false
    }

    _auto_nari(p_class, p_teban, to_y) {
        if (p_class == P_KEI) {
            if ((p_teban == TEBAN_SENTE) && (to_y < 2)) {return true}
            else if ((p_teban == TEBAN_GOTE) && (to_y > (BOARD_SIZE-3))) {return true}
        } else if ((p_class == P_KYO) || (p_class == P_FU)) {
            if ((p_teban == TEBAN_SENTE) && (to_y == 0)) {return true}
            else if ((p_teban == TEBAN_GOTE) && (to_y == (BOARD_SIZE-1))) {return true}
        }
        return false
    }

    _can_nari(p_class, p_teban, loc_from, loc_to) {
        const p_class_to = NARI_TO[p_class]
        if (p_class_to != p_class) {
            if (p_teban == TEBAN_SENTE) {
                if (Loc_y(loc_from) < 3 || Loc_y(loc_to) < 3) {
                    return true
                }
            } else {
                if (Loc_y(loc_from) > (BOARD_SIZE-4) || Loc_y(loc_to) > (BOARD_SIZE-4)) {
                    return true
                }
            }
        }
        return false
    }

    canmoves(loc) {
        const p = this.get(loc)
        if (p == P_EMPTY) {
            return []
        } else {
            const [y,x] = [Loc_y(loc), Loc_x(loc)]
            const p_class = Piece_class(p)
            const p_teban = Piece_teban(p)
            const mul = (p_teban == TEBAN_GOTE) ? -1 : 1
            const moves = []
            if (p_class == P_KEI) {
                const to_y = y + (mul * -2)
                const yxs = [[to_y, x - 1], [to_y, x + 1]]
                for (const yx of yxs) {
                    if (yx_in_board(yx[0],yx[1]) && (Piece_teban(this.get(Loc(yx[0],yx[1]))) != p_teban)) {
                        const loc_to = Loc(yx[0],yx[1])
                        const loc_from = Loc(y,x)
                        moves.push(new Move(p, loc_to, loc_from))
                        if (this._auto_nari(p_class, p_teban, yx[0])) {moves.at(-1).nari = true}
                        else if (this._can_nari(p_class, p_teban, loc_from, loc_to)) {
                            moves.push(new Move(p, loc_to, loc_from, true))
                        }
                    }
                }
            } else {
                const mat = MOVE_MATS[p_class]
                for (let i = 0; i < 8; i += 1) {
                    const n = mat[i]
                    for (let j = 1; j <= n; j += 1) {
                        const to_y = y + (YX_WAYS[i][0] * mul * j)
                        const to_x = x + (YX_WAYS[i][1] * mul * j)
                        const to_p = this.get(Loc(to_y, to_x))
                        if (yx_in_board(to_y, to_x)) {
                            if (Piece_teban(to_p) != p_teban) {
                                const loc_to = Loc(to_y,to_x)
                                const loc_from = Loc(y,x)
                                moves.push(new Move(p, loc_to, loc_from))
                                if (this._auto_nari(p_class, p_teban, to_y)) {moves.at(-1).nari = true}
                                else if (this._can_nari(p_class, p_teban, loc_from, loc_to)) {
                                    moves.push(new Move(p, loc_to, loc_from, true))
                                }
                            }
                            if (to_p != P_EMPTY) {break}
                        } else {break}
                    }
                }
            }
            return moves
        }
    }

    _canput_to(p_class, p_teban, loc_to, y, x) {
        if (this.get(loc_to) == P_EMPTY) {
            if (p_class == P_FU) {
                if ((y == 0) && (p_teban == TEBAN_SENTE)) {return false}
                if ((y == (BOARD_SIZE-1)) && (p_teban == TEBAN_GOTE)) {return false}
                if (this._get_fu_exist(p_teban, x)) {return false}
            } else if (p_class == P_KEI) {
                if ((y < 1) && (p_teban == TEBAN_SENTE)) {return false}
                if ((y > (BOARD_SIZE-2)) && (p_teban == TEBAN_GOTE)) {return false}
            }
            return true
        }
        return false
    }

    canputs(p) {
        const moves = []
        if (p != P_EMPTY) {
            const p_class = Piece_class(p)
            const p_teban = Piece_teban(p)
            for (let y = 0; y < BOARD_SIZE; y += 1) {
                for (let x = 0; x < BOARD_SIZE; x += 1) {
                    const loc_to = Loc(y, x)
                    if (this._canput_to(p_class, p_teban, loc_to, y, x)) {moves.push(new Move(p, loc_to))}
                }
            }
        }
        return moves
    }

    all_moves(teban) {
        let moves = []
        for (let y = 0; y < BOARD_SIZE; y += 1) {
            for (let x = 0; x < BOARD_SIZE; x += 1) {
                const loc = Loc(y,x)
                const p = this.get(loc)
                if (Piece_teban(p) == teban) {
                    moves = moves.concat(this.canmoves(loc))
                }
            }
        }
        for (const inhand of this.get_inhands(teban)) {
            moves = moves.concat(this.canputs(Piece(inhand[0], teban)))
        }
        return moves
    }

    status() {
        if (this._get_king_loc(TEBAN_GOTE) == 0xFF) {
            return STATUS_SENTE_WIN
        } else if (this._get_king_loc(TEBAN_SENTE) == 0xFF) {
            return STATUS_GOTE_WIN
        }
        return STATUS_NORMAL
    }
}

export class Phase {
    constructor(board) {
        this.moves = [board.all_moves(TEBAN_SENTE), board.all_moves(TEBAN_GOTE)]
        const can_attacks = new Uint8Array(BOARD_SIZE * BOARD_SIZE)
        can_attacks.fill(0)
        for (const s of this.moves) {
            for (const m of s) {
                const to_p = board.get(m.loc_to)
                if (to_p != P_EMPTY) {
                    const i = (Loc_y(m.loc_to) * BOARD_SIZE) + Loc_x(m.loc_to)
                    can_attacks[i] += 1
                }
            }
        }
        this.can_attacks = can_attacks
    }
    can_attack(y,x) {
        return this.can_attacks[(y * BOARD_SIZE) + x]
    }
}

export class Record {
    constructor(p, m) {
        this._teban = Piece_teban(p)
        this._class = Piece_class(p)
        this._move = m
    }
    get_teban(){
        return this._teban
    }
    get_class(){
        return this._class
    }
    get_move(){
        return this._move
    }
}

const BOARD_OPENING = new Board()
BOARD_OPENING
    .set(0x00, 0x17).set(0x01, 0x16).set(0x02, 0x15).set(0x03, 0x14).set(0x04, 0x11).set(0x05, 0x14).set(0x06, 0x15).set(0x07, 0x16).set(0x08, 0x17)
    .set(0x11, 0x12).set(0x17, 0x13)
    .set(0x20, 0x18).set(0x21, 0x18).set(0x22, 0x18).set(0x23, 0x18).set(0x24, 0x18).set(0x25, 0x18).set(0x26, 0x18).set(0x27, 0x18).set(0x28, 0x18)
    .set(0x60, 0x28).set(0x61, 0x28).set(0x62, 0x28).set(0x63, 0x28).set(0x64, 0x28).set(0x65, 0x28).set(0x66, 0x28).set(0x67, 0x28).set(0x68, 0x28)
    .set(0x71, 0x23).set(0x77, 0x22)
    .set(0x80, 0x27).set(0x81, 0x26).set(0x82, 0x25).set(0x83, 0x24).set(0x84, 0x21).set(0x85, 0x24).set(0x86, 0x25).set(0x87, 0x26).set(0x88, 0x27)
BOARD_OPENING._king_locs = 0x0484
BOARD_OPENING._fu_bits = (1 << 18) - 1
export function board_opening() {
    return BOARD_OPENING
}
